/*******************************************************************************
 * Copyright 2011-2012 Dik Grapendaal
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package sh.grapendaal.tsuushin.service.impl;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.jboss.netty.bootstrap.ClientBootstrap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import sh.grapendaal.tsuushin.core.BotFilter;
import sh.grapendaal.tsuushin.core.IrcBot;
import sh.grapendaal.tsuushin.core.IrcBot.DisconnectReason;
import sh.grapendaal.tsuushin.core.Network;
import sh.grapendaal.tsuushin.service.BotService;
import sh.grapendaal.tsuushin.service.CommandService;
import sh.grapendaal.tsuushin.service.ModuleService;
import sh.grapendaal.tsuushin.service.NetworkService;
import sh.grapendaal.tsuushin.service.PermissionService;
import sh.grapendaal.tsuushin.service.TrackingService;

/**
 * The Bot Service contains methods which deal with the IRC bots. This includes
 * retrieving a bot which meets certain specifications, creating new bots and
 * (re)connecting bots.
 */
@Singleton
public class BotServiceImpl implements BotService, IrcBot.ConnectListener {
	private static final Logger LOGGER = LoggerFactory.getLogger(BotServiceImpl.class);

	private static final int NUM_SCHEDULED_EXECUTOR_THREADS = 3;
	private static final long CONNECT_DELAY = 10L;

	@Inject
	private ModuleService moduleService;

	@Inject
	private NetworkService networkService;

	@Inject
	private PermissionService permissionService;

	@Inject
	private CommandService commandService;

	@Inject
	private TrackingService trackingService;

	private ScheduledThreadPoolExecutor executor;
	private List<IrcBot> connectedBots = new ArrayList<IrcBot>();
	private List<IrcBot> unconnectedBots = new ArrayList<IrcBot>();

	/**
	 * Starts the Bot Service.
	 * 
	 * @throws Exception
	 *             If the service was already started.
	 */
	public void start() {
		if (this.executor == null) {
			this.executor = new ScheduledThreadPoolExecutor(NUM_SCHEDULED_EXECUTOR_THREADS);
		}
	}

	/**
	 * Stops all running bots and shuts down the Bot Service.
	 */
	public void stop() {
		synchronized (this.connectedBots) {
			for (IrcBot bot : this.connectedBots) {
				bot.disconnect();
			}
		}

		if (this.executor != null) {
			this.executor.shutdown();
			this.executor = null;
		}
	}

	@Override
	public IrcBot createBot(String nickname, Network network) {
		IrcBot bot = new IrcBot();
		bot.getBotInformation().setNickname(nickname);
		bot.setNetwork(network);

		ClientBootstrap bootstrap = networkService.createBootstrap();
		bootstrap.getPipeline().addLast("handler", bot);

		bot.setBootstrap(bootstrap);

		// The order is important. Some services may interrupt the processing.
		bot.addListener(this);
		bot.addListener((TrackingServiceImpl) this.trackingService);
		bot.addListener((PermissionServiceImpl) this.permissionService);
		bot.addListener((CommandServiceImpl) this.commandService);
		bot.addListener((ModuleServiceImpl) this.moduleService);

		synchronized (this.unconnectedBots) {
			this.unconnectedBots.add(bot);
		}

		return bot;
	}

	@Override
	public void connectBots() {
		for (IrcBot bot : this.unconnectedBots) {
			scheduleConnect(bot);
		}

		synchronized (this.connectedBots) {
			this.connectedBots.addAll(this.unconnectedBots);
		}
		synchronized (this.unconnectedBots) {
			this.unconnectedBots.clear();
		}
	}

	@Override
	public void scheduleConnect(final IrcBot bot) {
		if (!this.unconnectedBots.contains(bot)) {
			throw new IllegalArgumentException("The supplied bot is not disconnected.");
		}

		this.executor.schedule(new ConnectBotTask(bot), CONNECT_DELAY, TimeUnit.SECONDS);

		LOGGER.debug("Scheduled bot {}.", bot);
	}

	@Override
	public BotFilter filter() {
		return new BotFilter(this.connectedBots);
	}

	@Override
	public void onDisconnect(IrcBot bot, DisconnectReason reason) {
		synchronized (this.connectedBots) {
			this.connectedBots.remove(bot);
		}
		synchronized (this.unconnectedBots) {
			this.unconnectedBots.add(bot);
		}

		if (reason != DisconnectReason.CONNECTION_CLOSED) {
			scheduleConnect(bot);
		}
	}

	@Override
	public void onConnect(IrcBot bot) {
		synchronized (this.unconnectedBots) {
			this.unconnectedBots.remove(bot);
		}
		synchronized (this.connectedBots) {
			this.connectedBots.add(bot);
		}
	}

	private static class ConnectBotTask implements Runnable {
		private IrcBot bot;

		public ConnectBotTask(IrcBot bot) {
			this.bot = bot;
		}

		@Override
		public void run() {
			try {
				bot.connect();
			} catch (IOException e) {
				LOGGER.error("Bot could not connect.", e);
			}
		}

	}
}
